一、JNI 在 Android 中的角色
JNI(Java Native Interface)是 Java 虚拟机规范的一部分,定义了 Java 代码与 C/C++ 原生代码之间互操作的接口。在 Android 中,JNI 是连接 Java 世界(Android Framework、App 代码)和 Native 世界(C/C++ 库、硬件驱动、NDK 模块)的桥梁。
Android 系统本身大量使用 JNI:Framework 层通过 JNI 调用 HAL(硬件抽象层)实现对传感器、摄像头、音频等硬件的访问。Zygote 进程在 fork 每个应用进程时,会预先加载 JNI 库以减少应用启动时的开销。
JNI 在 Android 中的典型使用场景:
- 性能敏感:图像处理、音视频编解码、加密算法。
- 代码复用:移植已有的 C/C++ 库(如 ffmpeg、OpenSSL、SDL)。
- 安全需求:将加解密、反作弊等关键逻辑放在 native 层增加逆向难度。
- 系统调用:访问 Linux 底层 API(epoll、mmap、ptrace 等)。
1.1 JNIEnv 与 JavaVM 的内部结构
理解 JNIEnv 的本质对于写出正确的 JNI 代码至关重要。JNIEnv 是一个函数指针表(在 C 中是一个 const struct JNINativeInterface*),包含了所有 JNI 函数的入口。每个线程有自己的 JNIEnv,这意味着:
- 不能跨线程传递 JNIEnv 指针。
- 在 native 创建的线程中必须通过
AttachCurrentThread 获取当前线程的 JNIEnv。
struct JNINativeInterface { jclass (*FindClass)(JNIEnv*, const char*); jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*); };
|
JavaVM 是一个进程级的单例,代表整个 Java 虚拟机实例。在 Android 进程中,一个进程只有一个 JavaVM。可以通过 JNI_OnLoad 缓存 JavaVM 指针,供 native 线程使用。
二、JNI 数据类型映射
JNI 定义了一组与 Java 类型对应的 C/C++ 类型:
| Java 类型 |
JNI 类型 |
C/C++ 类型 |
描述 |
boolean |
jboolean |
unsigned char (8-bit) |
0 = false, nonzero = true |
byte |
jbyte |
signed char (8-bit) |
-128 to 127 |
char |
jchar |
unsigned short (16-bit) |
UTF-16 code unit |
short |
jshort |
signed short (16-bit) |
|
int |
jint |
signed int (32-bit) |
|
long |
jlong |
signed long long (64-bit) |
|
float |
jfloat |
float (32-bit IEEE 754) |
|
double |
jdouble |
double (64-bit IEEE 754) |
|
void |
void |
N/A |
|
Object |
jobject |
pointer |
所有 Java 对象的基类型 |
String |
jstring |
pointer |
Java String 对象 |
Class |
jclass |
pointer |
Java Class 对象 |
Throwable |
jthrowable |
pointer |
Java 异常对象 |
boolean[] |
jbooleanArray |
pointer |
|
int[] |
jintArray |
pointer |
|
引用类型是一个指针(在 ART 运行时中指向堆上的对象),不能直接当作 C 指针使用。访问 Java 数组中的数据需要通过 JNI 提供的 Get/Release 函数。
JNIEXPORT void JNICALL Java_com_example_NativeLib_processArray( JNIEnv *env, jobject thiz, jintArray array) {
jsize len = (*env)->GetArrayLength(env, array);
jint *elements = (*env)->GetIntArrayElements(env, array, NULL); if (elements == NULL) { return; }
for (int i = 0; i < len; i++) { elements[i] *= 2; }
(*env)->ReleaseIntArrayElements(env, array, elements, 0); }
|
GetPrimitiveArrayCritical 提供了更直接的访问方式(可能阻止 GC),配对使用的是 ReleasePrimitiveArrayCritical。Critical 版本与 Get/Release 版本的区别是:Critical 期间 GC 被暂停(对性能敏感的代码慎用),且不能在这两个调用之间调用其他 JNI 函数或阻塞。
2.1 GetStringUTFChars 与内存管理
字符串处理是 JNI 中最常见的操作之一,也是最容易产生内存泄漏的地方:
JNIEXPORT void JNICALL Java_com_example_NativeLib_processString( JNIEnv *env, jobject thiz, jstring input) {
const char *utf = (*env)->GetStringUTFChars(env, input, NULL); if (utf == NULL) { return; }
size_t len = strlen(utf);
(*env)->ReleaseStringUTFChars(env, input, utf);
jsize utf_len = (*env)->GetStringUTFLength(env, input);
jstring result = (*env)->NewStringUTF(env, "Hello from native");
const jchar *critical = (*env)->GetStringCritical(env, input, NULL); if (critical != NULL) { (*env)->ReleaseStringCritical(env, input, critical); } }
|
三、局部引用与全局引用:引用表溢出
JNI 的引用管理是面试中的高频考点,也是 native 内存泄漏的常见来源。
3.1 三种引用类型
| 引用类型 |
创建方式 |
生命周期 |
使用场景 |
| 局部引用(Local Reference) |
大多数 JNI 函数自动创建 |
当前 native 方法返回时自动释放 |
临时使用的 Java 对象 |
| 全局引用(Global Reference) |
NewGlobalRef() |
显式调用 DeleteGlobalRef() 释放 |
跨多个 native 调用共享的对象 |
| 弱全局引用(Weak Global Reference) |
NewWeakGlobalRef() |
显式调用 DeleteWeakGlobalRef() 释放 |
可能被 GC 回收的对象引用 |
3.2 局部引用表溢出
每个 native 线程有一个局部引用表,默认容量通常是 512(ART 运行时)。如果在一个 native 方法中创建了大量局部引用而不释放,会抛出 JNI ERROR (app bug): local reference table overflow:
JNIEXPORT void JNICALL Java_com_example_NativeLib_processList( JNIEnv *env, jobject thiz, jobjectArray strings) { jsize len = (*env)->GetArrayLength(env, strings); for (int i = 0; i < len; i++) { jstring str = (jstring)(*env)->GetObjectArrayElement(env, strings, i); const char *utf = (*env)->GetStringUTFChars(env, str, NULL); (*env)->ReleaseStringUTFChars(env, str, utf); } }
|
正确的处理方式:
JNIEXPORT void JNICALL Java_com_example_NativeLib_processList( JNIEnv *env, jobject thiz, jobjectArray strings) { jsize len = (*env)->GetArrayLength(env, strings); for (int i = 0; i < len; i++) { jstring str = (jstring)(*env)->GetObjectArrayElement(env, strings, i); const char *utf = (*env)->GetStringUTFChars(env, str, NULL); process(utf); (*env)->ReleaseStringUTFChars(env, str, utf); (*env)->DeleteLocalRef(env, str); } }
JNIEXPORT void JNICALL Java_com_example_NativeLib_processList2( JNIEnv *env, jobject thiz, jobjectArray strings) { jsize len = (*env)->GetArrayLength(env, strings); for (int i = 0; i < len; i++) { if ((*env)->PushLocalFrame(env, 16) < 0) { return; } jstring str = (jstring)(*env)->GetObjectArrayElement(env, strings, i); const char *utf = (*env)->GetStringUTFChars(env, str, NULL); process(utf); (*env)->ReleaseStringUTFChars(env, str, utf); (*env)->PopLocalFrame(env, NULL); } }
|
3.3 全局引用
static jobject g_cached_object = NULL;
JNIEXPORT void JNICALL Java_com_example_NativeLib_init(JNIEnv *env, jobject thiz, jobject obj) { g_cached_object = (*env)->NewGlobalRef(env, obj); }
JNIEXPORT void JNICALL Java_com_example_NativeLib_cleanup(JNIEnv *env, jobject thiz) { if (g_cached_object != NULL) { (*env)->DeleteGlobalRef(env, g_cached_object); g_cached_object = NULL; } }
|
3.4 全局引用的容量限制
全局引用表也有容量限制(ART 中默认约为 51200 个全局引用)。如果创建了过多全局引用且不释放,会导致 global reference table overflow。这是比局部引用表溢出更难排查的问题——因为全局引用是跨方法的,泄漏会随着时间累积。
四、JNI_OnLoad 与动态注册
有两种注册 native 方法的方式:
4.1 静态注册(基于方法名约定)
JNIEXPORT jstring JNICALL Java_com_example_NativeLib_getMessage(JNIEnv *env, jobject thiz) { return (*env)->NewStringUTF(env, "Hello from JNI"); }
|
缺点:方法名太长,容易写错,且每次调用 native 方法时 VM 需要搜索符号表。
4.2 动态注册(通过 JNI_OnLoad)
static jstring native_getMessage(JNIEnv *env, jobject thiz) { return (*env)->NewStringUTF(env, "Hello from JNI"); }
static jint native_add(JNIEnv *env, jobject thiz, jint a, jint b) { return a + b; }
static const JNINativeMethod gMethods[] = { {"getMessage", "()Ljava/lang/String;", (void *)native_getMessage}, {"add", "(II)I", (void *)native_add}, };
jint JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv *env = NULL; if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; }
jclass clazz = (*env)->FindClass(env, "com/example/NativeLib"); if (clazz == NULL) { return JNI_ERR; }
if ((*env)->RegisterNatives(env, clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])) < 0) { return JNI_ERR; }
return JNI_VERSION_1_6; }
|
动态注册的优势:
- 方法名更简洁,不需要长命名。
- 首次加载时批量注册,后续调用查找快。
- 可以在
JNI_OnLoad 中做初始化工作(如缓存 MethodID)。
JNI 方法签名规则:
(参数类型签名)返回值类型签名
I = int, J = long, F = float, D = double, Z = boolean, V = void
Ljava/lang/String; = String 对象
[I = int[]
L 开头的类名必须以 ; 结尾,如 Ljava/util/List;
- 构造函数的返回值类型为
V,方法名为 <init>
4.3 JNIEXPORT 与 JNICALL 的含义
4.4 JNI_OnUnload
void JNI_OnUnload(JavaVM *vm, void *reserved) { JNIEnv *env; (*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6);
if (g_cached_class != NULL) { (*env)->DeleteGlobalRef(env, g_cached_class); } }
|
五、从 Native 调用 Java 方法
JNI 允许 native 代码调用 Java 方法。关键 API:GetMethodID/GetStaticMethodID + CallVoidMethod/CallStaticMethod 等。
static void callJavaCallback(JNIEnv *env, jobject callback_obj, const char *result) { jclass clazz = (*env)->GetObjectClass(env, callback_obj);
jmethodID methodId = (*env)->GetMethodID(env, clazz, "onResult", "(Ljava/lang/String;)V");
jstring jResult = (*env)->NewStringUTF(env, result);
(*env)->CallVoidMethod(env, callback_obj, methodId, jResult);
(*env)->DeleteLocalRef(env, jResult); (*env)->DeleteLocalRef(env, clazz); }
|
性能优化要点:FindClass、GetMethodID、GetFieldID 的调用开销较大(需要 JNI 内部字符串比较和查找),应该在 JNI_OnLoad 中一次性获取并缓存为全局引用:
static jclass g_callbackClass = NULL; static jmethodID g_onResultMethod = NULL;
jint JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv *env; (*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6); jclass localClass = (*env)->FindClass(env, "com/example/Callback"); g_callbackClass = (*env)->NewGlobalRef(env, localClass); g_onResultMethod = (*env)->GetMethodID(env, g_callbackClass, "onResult", "(Ljava/lang/String;)V"); (*env)->DeleteLocalRef(env, localClass); return JNI_VERSION_1_6; }
|
5.1 jmethodID 的缓存策略
MethodID 和 FieldID 是进程级有效的,不需要全局引用。但 jclass 必须用全局引用缓存。ID 本身在 ART 中是 ArtMethod* 或 ArtField* 的指针,只要类没有被卸载就不会失效。在 Android 中,应用类几乎从不卸载,所以 ID 的缓存非常安全。
5.2 调用静态方法和访问静态字段
jclass clazz = (*env)->FindClass(env, "java/lang/System"); jmethodID gcMethod = (*env)->GetStaticMethodID(env, clazz, "gc", "()V"); (*env)->CallStaticVoidMethod(env, clazz, gcMethod);
jclass buildClass = (*env)->FindClass(env, "android/os/Build"); jfieldID modelField = (*env)->GetStaticFieldID(env, buildClass, "MODEL", "Ljava/lang/String;"); jstring model = (jstring)(*env)->GetStaticObjectField(env, buildClass, modelField);
|
六、Native 线程与 JavaVM
从 native 创建的线程(如通过 pthread 或 std::thread),JNIEnv 是线程局部存储的。新线程不能直接使用从其他线程传入的 JNIEnv 指针。必须调用 JavaVM::AttachCurrentThread() 获取当前线程的 JNIEnv:
static JavaVM *g_jvm = NULL;
jint JNI_OnLoad(JavaVM *vm, void *reserved) { g_jvm = vm; return JNI_VERSION_1_6; }
void *thread_func(void *arg) { JNIEnv *env; jint result = (*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL); if (result != JNI_OK) { return NULL; }
(*g_jvm)->DetachCurrentThread(g_jvm); return NULL; }
|
注意事项:
AttachCurrentThread 必须成对出现 DetachCurrentThread,否则线程退出后局部引用表无法被清理。
- 如果线程已经被 attached,再次调用
AttachCurrentThread 是安全的(no-op)。
- 一旦 detach,该线程就不能再使用 JNI 函数,除非重新 attach。
- 大量线程频繁 attach/detach 会影响性能——考虑使用线程池。
6.1 Attach 时的线程属性
JavaVMAttachArgs attach_args; attach_args.version = JNI_VERSION_1_6; attach_args.name = "MyNativeWorker"; attach_args.group = NULL; (*g_jvm)->AttachCurrentThread(g_jvm, &env, &attach_args);
|
6.2 不要在 native 线程中 FindClass
在 native 创建的线程中调用 FindClass 会使用系统 ClassLoader(Bootstrap ClassLoader),导致找不到应用自定义的类。解决方案是在 JNI_OnLoad 中缓存 ClassLoader 并使用它来加载类:
static jobject g_classLoader = NULL; static jmethodID g_findClassMethod = NULL;
jint JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv *env; (*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6);
jclass clazz = (*env)->FindClass(env, "com/example/NativeLib"); jclass classLoaderClass = (*env)->FindClass(env, "java/lang/ClassLoader"); jmethodID getClassLoaderMethod = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, clazz), "getClassLoader", "()Ljava/lang/ClassLoader;"); jobject classLoader = (*env)->CallObjectMethod(env, clazz, getClassLoaderMethod);
g_classLoader = (*env)->NewGlobalRef(env, classLoader); g_findClassMethod = (*env)->GetMethodID(env, classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
return JNI_VERSION_1_6; }
jclass findClassInNativeThread(JNIEnv *env, const char *name) { jstring className = (*env)->NewStringUTF(env, name); jclass clazz = (jclass)(*env)->CallObjectMethod(env, g_classLoader, g_findClassMethod, className); (*env)->DeleteLocalRef(env, className); return clazz; }
|
七、Java 异常的 Native 处理
JNI 函数出错时,大多数不会通过返回值表示,而是在 Java 层设置异常。native 代码必须显式检查和清理这些异常:
JNIEXPORT void JNICALL Java_com_example_NativeLib_safeCall(JNIEnv *env, jobject thiz, jobject obj) { jclass clazz = (*env)->GetObjectClass(env, obj); jmethodID methodId = (*env)->GetMethodID(env, clazz, "nonExistentMethod", "()V");
if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); jclass npeClass = (*env)->FindClass(env, "java/lang/NullPointerException"); (*env)->ThrowNew(env, npeClass, "Callback method not found"); return; }
(*env)->CallVoidMethod(env, obj, methodId); }
|
关键规则:大多数 JNI 函数在异常挂起时如果继续调用会失败(甚至崩溃)。通过 ExceptionCheck() 或 ExceptionOccurred() 检测异常,然后 ExceptionClear() 清除或 ThrowNew() 抛出新异常。
7.1 异常的详细处理
if ((*env)->ExceptionCheck(env)) { jthrowable exception = (*env)->ExceptionOccurred(env); if (exception != NULL) { jclass exceptionClass = (*env)->GetObjectClass(env, exception); jmethodID getMessageMethod = (*env)->GetMethodID(env, exceptionClass, "getMessage", "()Ljava/lang/String;"); jstring message = (jstring)(*env)->CallObjectMethod(env, exception, getMessageMethod);
(*env)->ExceptionClear(env);
jclass runtimeEx = (*env)->FindClass(env, "java/lang/RuntimeException"); const char *msg = (*env)->GetStringUTFChars(env, message, NULL); (*env)->ThrowNew(env, runtimeEx, msg); (*env)->ReleaseStringUTFChars(env, message, msg);
(*env)->DeleteLocalRef(env, message); (*env)->DeleteLocalRef(env, exceptionClass); (*env)->DeleteLocalRef(env, exception); } }
|
7.2 native 代码中的崩溃与异常的区别
Native 崩溃(SIGSEGV、SIGABRT 等)不等于 Java 异常。native 层的空指针解引用直接导致进程收到 SIGSEGV 信号,触发 tombstone 生成和进程退出——它不会转换为 Java 层的 NullPointerException。只有通过 JNI 调用的 Java 方法失败(如找不到方法、类型不匹配)才会产生 Java 异常。
八、完整实践:Native 加密实现
#include <jni.h> #include <string.h> #include <openssl/aes.h>
static jbyteArray native_encrypt(JNIEnv *env, jobject thiz, jbyteArray data, jbyteArray key) { jsize dataLen = (*env)->GetArrayLength(env, data); jsize keyLen = (*env)->GetArrayLength(env, key);
if (keyLen != AES_BLOCK_SIZE) { jclass exClass = (*env)->FindClass(env, "java/lang/IllegalArgumentException"); (*env)->ThrowNew(env, exClass, "Key must be 16 bytes (AES-128)"); return NULL; }
jbyte *dataBytes = (*env)->GetByteArrayElements(env, data, NULL); jbyte *keyBytes = (*env)->GetByteArrayElements(env, key, NULL);
jsize paddedLen = ((dataLen / AES_BLOCK_SIZE) + 1) * AES_BLOCK_SIZE; unsigned char *paddedData = malloc(paddedLen); memcpy(paddedData, (unsigned char *)dataBytes, dataLen); int padValue = paddedLen - dataLen; memset(paddedData + dataLen, padValue, padValue);
AES_KEY aesKey; AES_set_encrypt_key((const unsigned char *)keyBytes, 128, &aesKey);
unsigned char iv[AES_BLOCK_SIZE] = {0}; unsigned char *ciphertext = malloc(paddedLen); AES_cbc_encrypt(paddedData, ciphertext, paddedLen, &aesKey, iv, AES_ENCRYPT);
jbyteArray result = (*env)->NewByteArray(env, paddedLen); (*env)->SetByteArrayRegion(env, result, 0, paddedLen, (jbyte *)ciphertext);
free(ciphertext); free(paddedData); (*env)->ReleaseByteArrayElements(env, key, keyBytes, JNI_ABORT); (*env)->ReleaseByteArrayElements(env, data, dataBytes, JNI_ABORT);
return result; }
static const JNINativeMethod gMethods[] = { {"encrypt", "([B[B)[B", (void *)native_encrypt}, };
jint JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv *env; (*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6); jclass clazz = (*env)->FindClass(env, "com/example/crypto/NativeCrypto"); (*env)->RegisterNatives(env, clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])); return JNI_VERSION_1_6; }
|
九、JNI 性能优化总结
| 优化点 |
说明 |
| 缓存 jclass/jmethodID/jfieldID |
在 JNI_OnLoad 中一次获取,避免重复查找 |
| 使用 RegisterNatives |
比静态注册更快 |
| 避免频繁的 JNI 调用边界跨越 |
将多次小操作合并为一次大数据传递 |
| 在 native 侧批量操作数据 |
如数组处理在 native 循环中完成,不要每个元素都回传 Java |
| 使用 GetPrimitiveArrayCritical |
对性能敏感的数组访问(但要短小,不能阻塞) |
| 管理引用 |
及时 DeleteLocalRef,避免引用表溢出 |
| 使用 PushLocalFrame |
对大量临时引用比手动 DeleteLocalRef 更方便 |
十、面试常问题目
Q1: 局部引用和全局引用的区别?什么情况下会导致 local reference table overflow?
局部引用在 native 方法返回时自动释放,全局引用需要显式 DeleteGlobalRef() 才能释放。在循环中创建大量局部引用(如遍历大数组时每次都 GetObjectArrayElement)而不手动 DeleteLocalRef,或在循环内不对引用进行释放,会导致局部引用表(默认 512 容量)溢出。解决方案是使用 PushLocalFrame/PopLocalFrame 或在循环内 DeleteLocalRef。
Q2: JNI_OnLoad 的作用是什么?什么时候需要实现它?
JNI_OnLoad 在 System.loadLibrary() 加载 .so 文件后由 VM 自动调用。主要用于:(1) 使用 RegisterNatives() 进行动态注册,替代冗长的 Java_xxx 命名;(2) 初始化全局引用(缓存 class、methodID、fieldID);(3) 缓存 JavaVM 指针供 native 线程使用;(4) 返回所需的 JNI 版本号。
Q3: 为什么 native 线程不能直接使用传入的 JNIEnv 指针?
JNIEnv 是线程局部存储的(thread-local),与创建它的线程绑定。如果在 Thread A 获取了一个 JNIEnv 指针,然后在 Thread B(通过 pthread_create 创建的 native 线程)中使用它,会出现两种情况:(1) 使用错误的线程局部数据导致崩溃或未定义行为;(2) 该 JNIEnv 对应的线程局部引用表是为 Thread A 管理的,Thread B 没有权限访问。正确方法是新线程调用 JavaVM::AttachCurrentThread() 获取自己的 JNIEnv。
Q4: JNI 调用 Java 方法时,methodID 可以跨线程使用吗?需要全局引用吗?
MethodID 和 FieldID 是进程范围内的,可以跨线程安全使用,不需要转换为全局引用。这是因为它们在 VM 内部是以指针或索引形式存在的,不受 GC 影响。但 class 引用(jclass)必须转换为全局引用才能跨线程使用或缓存——局部 jclass 在方法返回后可能失效。
Q5: 在 native 线程中 FindClass 为什么可能失败?如何解决?
在 native 创建的 pthread 中调用 FindClass 使用的是系统 ClassLoader(Bootstrap ClassLoader),该 ClassLoader 只能找到 Framework 类(如 java.lang.String),找不到应用自定义的类。解决方案:(1) 在 JNI_OnLoad(运行在主线程中)时缓存应用 ClassLoader 的 GlobalRef;(2) 在 native 线程中通过缓存的 ClassLoader.loadClass() 加载类;(3) 或者在 JNI_OnLoad 时就缓存好所有需要的 jclass 全局引用,native 线程中直接使用。
参考源码路径:
- JNI 规范:
https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html
- Android JNI Tips:
https://developer.android.com/training/articles/perf-jni
- AOSP 示例:
frameworks/base/core/jni/android_util_EventLog.cpp
- AOSP JNI 入口:
libnativehelper/include_jni/jni.h
- ART 运行时 JNI 实现:
art/runtime/jni/jni_internal.cc